﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using Tsanie.ShellExtension;

namespace Tsanie.Mp3Tag {
    public class ID3v2 : TagBase {
        private byte[] _b;
        private Encoding _encoding;
        private FileStream _fs;
        private List<Frame> _frames;
        private long _dataOffset;

        public int TagCount { get { return _frames.Count; } }
        public Frame this[int index] { get { return _frames[index]; } }
        public Frame this[string ID] {
            get {
                for (int i = 0; i < _frames.Count; i++) {
                    Frame f = _frames[i];
                    if (f.FrameID == ID)
                        return f;
                }
                return null;
            }
        }

        private ID3v2() {
            _b = new byte[4];
            _dataOffset = 0;
            _frames = new List<Frame>();
            _encoding = null;
        }
        public ID3v2(FileStream fs)
            : this() {
            _fs = fs;
            Read();
        }
        public ID3v2(string filename)
            : this() {
            _fs = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        }

        public byte[] PictureData {
            get {
                Frame pic = this["APIC"];
                if (pic != null)
                    return pic.Data;
                return null;
            }
            set {
                Frame pic = this["APIC"];
                if (pic == null)
                    pic = this.Add(new Frame() { FrameID = "APIC" });
                pic.Data = value;
            }
        }
        public Encoding Encoding { set { _encoding = value; } }

        #region TagBase Members

        public override string Title {
            get { return this["TIT2"].DataString(_encoding); }
            set {
                Frame frame = this["TIT2"];
                if (frame == null)
                    frame = this.Add(new Frame() { FrameID = "TIT2" });
                frame.Data = value.ToTagBytes();
            }
        }
        public override string Artist {
            get { return this["TPE1"].DataString(_encoding); }
            set {
                Frame frame = this["TPE1"];
                if (frame == null)
                    frame = this.Add(new Frame() { FrameID = "TPE1" });
                frame.Data = value.ToTagBytes();
            }
        }
        public override string Album {
            get { return this["TALB"].DataString(_encoding); }
            set {
                Frame frame = this["TALB"];
                if (frame == null)
                    frame = this.Add(new Frame() { FrameID = "TALB" });
                frame.Data = value.ToTagBytes();
            }
        }
        public override string Year {
            get { return this["TYER"].DataString(); }
            set {
                Frame frame = this["TYER"];
                if (frame == null)
                    frame = this.Add(new Frame() { FrameID = "TYER" });
                frame.Data = value.ToTagBytes(Encoding.ASCII);
            }
        }
        public override string Comments {
            get { return this["COMM"].DataString(_encoding); }
            set {
                Frame frame = this["COMM"];
                if (frame == null)
                    frame = this.Add(new Frame() { FrameID = "COMM" });
                frame.Data = value.ToTagBytes();
            }
        }
        public override string Track {
            get { return this["TRCK"].DataString(); }
            set {
                Frame frame = this["TRCK"];
                if (frame == null)
                    frame = this.Add(new Frame() { FrameID = "TRCK" });
                frame.Data = value.ToTagBytes(Encoding.ASCII);
            }
        }
        public override string Genre {
            get { return TagUtil.GetGenre(this["TCON"].DataString()); }
            set {
                foreach (KeyValuePair<int, string> kv in Definition.Genres) {
                    if (kv.Value == value) {
                        Frame frame = this["TCON"];
                        if (frame == null)
                            frame = this.Add(new Frame() { FrameID = "TCON" });
                        frame.Data = ("(" + kv.Key + ")").ToTagBytes(Encoding.ASCII);
                    }
                }
            }
        }

        public override void Read() {
            _fs.Seek(5, SeekOrigin.Begin);
            _fs.Read(_b, 1);  // Flags
            bool ext = ((_b[0] & 64) == 64);
            _fs.Read(_b, 4);  // ID3v2 Size
            _dataOffset = _b.ToTagInt();
            if (ext)        // 有 Extended Header
                _fs.Seek(10, SeekOrigin.Current);
            int count = 0;
            while (count < _dataOffset) {
                // 遍历Tag
                Frame f = Frame.ReadFrame(_fs);
                if (f == null)
                    break;
                _frames.Add(f);
                count += f.FrameSize + 10;
            }
        }
        public override void Write() {
            if (_fs == null)
                return;
            _fs.Close();
            int frameSize = 0;
            foreach (Frame f in _frames) {
                frameSize += 10 + f.FrameSize;
            }
            if (frameSize <= _dataOffset) {
                // ID3v2 预留空间足够，不需要重写整个文件
                _fs = new FileStream(_fs.Name, FileMode.Open, FileAccess.Write, FileShare.Read);
                WriteTag(_dataOffset, _fs);
                byte[] padding = new byte[_dataOffset - frameSize];
                _fs.Write(padding, 0, padding.Length);
#if TRACE
                Common.OutputDebugString("Write(void):: overwrite tags.");
#endif
            } else {
                // 重写整个文件
                _fs = new FileStream(_fs.Name, FileMode.Open, FileAccess.Read, FileShare.Read);
                _fs.Seek(_dataOffset, SeekOrigin.Begin);
                byte[] buffer = new byte[1024];
                int count;
                MemoryStream bufferStream = new MemoryStream();
                while ((count = _fs.Read(buffer, 0, buffer.Length)) > 0) {
                    bufferStream.Write(buffer, 0, count);
                }
                _fs.Close();
                // Padding
                int padding = 0x400 - ((frameSize + 10) % 0x400);
                _dataOffset = frameSize + padding;
                // start
                _fs = new FileStream(_fs.Name, FileMode.Create, FileAccess.Write, FileShare.Read);
                WriteTag(_dataOffset, _fs);
                _fs.Write(new byte[padding], 0, padding);
                // 加上数据
                _fs.Write(bufferStream.ToArray(), 0, (int)bufferStream.Length);
#if TRACE
                Common.OutputDebugString("Write(void):: rewrite whole file.");
#endif
            }
            _fs.Flush();
            _fs.Close();
        }

        private void WriteTag(long frameSize, FileStream fs) {
            fs.Write(new byte[] { 0x49, 0x44, 0x33, 0x03, 0, 0 }, 0, 6);   // Header flag
            fs.Write(((int)frameSize).ToTagIntBytes(), 0, 4); // Total Frames size
            WriteFrames();
        }
        private void WriteFrames() {
            foreach (Frame f in _frames) {
                _fs.Write(f.FrameIDBytes, 0, 4);
                _fs.Write(f.FrameSizeBytes, 0, 4);
                _fs.Write(f.FlagsBytes, 0, 2);
                _fs.Write(f.Data, 0, f.Data.Length);
            }
        }

        #endregion

        public Frame Add(Frame f) {
            _frames.Add(f);
            return f;
        }

        public void Remove(string ID) {
            for (int i = 0; i < _frames.Count; i++) {
                if (_frames[i].FrameID == ID) {
                    _frames.RemoveAt(i);
                    return;
                }
            }
        }
        public void RemoveAt(int index) {
            _frames.RemoveAt(index);
        }
    }

    public class Frame {
        public static byte[] buffer = new byte[4];
        public static Frame ReadFrame(FileStream stream) {
            stream.Read(buffer, 4);
            if (buffer[0] == 0 || buffer[1] == 0 || buffer[2] == 0 || buffer[3] == 0) {
                return null;
            }
            Frame f = new Frame();
            f._FrameID = buffer.ToID();
            Array.Copy(buffer, 0, f._frameID, 0, 4);
            stream.Read(buffer, 4);
            f._FrameSize = buffer.ToInt();
            Array.Copy(buffer, 0, f._frameSize, 0, 4);
            stream.Read(buffer, 2);
            f._Flags = buffer.ToShort();
            Array.Copy(buffer, 0, f._flags, 0, 2);
            f._DataOffset = stream.Position;
            f._Data = new byte[f._FrameSize];
            stream.Read(f._Data, f._FrameSize);
            return f;
        }

        private string _FrameID;
        private int _FrameSize;
        private short _Flags;
        private long _DataOffset;
        internal byte[] _Data;
        private byte[] _frameID = new byte[4];
        private byte[] _frameSize = new byte[4];
        private byte[] _flags = new byte[2];

        public string FrameID {
            get { return this._FrameID; }
            set {
                this._FrameID = value;
                this._frameID = Encoding.ASCII.GetBytes(value);
            }
        }
        public int FrameSize {
            get { return this._FrameSize; }
            set {
                this._FrameSize = value;
                this._frameSize = value.ToIntBytes();
            }
        }
        public short Flags {
            get { return this._Flags; }
            set {
                this._Flags = value;
                this._flags = new byte[] { (byte)((value >> 8) & 0xff), (byte)(value & 0xff) };
            }
        }
        public long DataOffset {
            get { return this._DataOffset; }
        }
        public byte[] Data {
            get { return this._Data; }
            set {
                this._Data = value;
                this.FrameSize = value.Length;
            }
        }
        public byte[] FrameIDBytes { get { return this._frameID; } }
        public byte[] FrameSizeBytes { get { return this._frameSize; } }
        public byte[] FlagsBytes { get { return this._flags; } }

        public override string ToString() {
            return string.Format("{0} {1}: 0x{2:X8}", this.FrameID, this.FrameSize.ToString().PadLeft(8), this.DataOffset);
        }
    }

    static class FrameExtension {
        public static string DataString(this Frame frame) {
            return frame.DataString(null);
        }
        public static string DataString(this Frame frame, Encoding encoding) {
            if (frame == null)
                return null;
            return frame._Data.ToTagString(encoding);
        }
    }

}
